// ###########################################################################################################################
// Establishes the DreamSpace event system, which uses the browser's own event system where possible.  This allows window
// "on error" hooks for proper reporting of filename, call stack, and position details in the error message.
// ###########################################################################################################################

module DreamSpace.System.Events {
    // =======================================================================================================================
    // Basic Event Management - Uses the host's native even system.
    // (Inspiration from article by Dean Edwards: http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/)

    export interface EventHandler { (_this: {}, ...args): any };

    export interface EventTriggerCallback<TOwner extends Object, TCallback extends EventHandler> { (event: EventDispatcher.$Type<TOwner, TCallback>, handler: IDelegate<TOwner, TCallback>): any }

    interface __IEventHandlerInfo<TOwner extends Object, TCallback extends EventHandler> extends IDelegate<TOwner, TCallback> {
        __internalCallback: (ev: any) => any;
        addFunctionName: string;
        eventMode: EventModes;
    };
    export interface __IEventDispatcherParent { __parent: __IEventDispatcherParent }

    export enum EventModes {
        /** Trigger event on the way up to the target. */
        Capture,
        /** Trigger event on the way down from the target. */
        Bubble,
        /** Trigger event on both the way up to the target, then back down again. */
        CaptureAndBubble
    };

    /** Returns a new event dispatcher instance. */
    export function EventDispatcher<TOwner extends IClassModule<Object>, TCallback extends EventHandler>(owner?: TOwner, eventName?: string, removeOnTrigger?: boolean, eventTriggerCallback?: EventTriggerCallback<TOwner, TCallback>, canCancel?: boolean): EventDispatcher.$Type<TOwner, TCallback> { }
    /** Creates an event EventDispatcher that can store multiple handlers (similar to events in C#).
      * The event object attempts to register and trigger events using the browser's native
      * event system, and if not available (i.e. server side), then the event system is
      * simulated, and any errors caused by handlers will be placed in the 'errors' property
      * for the owner to review.
      * TOwner: The type of the object that will own the event.
      * TCallbackType: This is the expected handler signature for the event.
      */
    export module EventDispatcher {
        export function getPropertyNameFromEventName(eventName: string): string {
            return eventName.match(/^on[^a-z]/) ? eventName : "on" + eventName.charAt(0).toUpperCase() + eventName.substring(1);
        }

        /** Registers a supported event on a graph item type. */
        export function registerEvent<TOwner extends IClassModule<Object>, TCallback extends EventHandler>(type: { new (...args): TOwner }, eventName: string, eventMode: EventModes = EventModes.Capture, removeOnTrigger: boolean = false, eventTriggerCallback?: EventTriggerCallback<TOwner, TCallback>, customEventPropName?: string)
            : { _eventMode: EventModes; _eventName: string; _removeOnTrigger: boolean; eventFuncType: () => EventDispatcher.$Type<TOwner, TCallback>; eventPropertyType: EventDispatcher.$Type<TOwner, TCallback> } {

            customEventPropName || (customEventPropName = EventDispatcher.getPropertyNameFromEventName(eventName)); // (fix to support the convention of {item}.on{Event}().
            var privateEventName = "__" + eventName + "Event";

            var onEventProxy = function (): EventDispatcher.$Type<TOwner, TCallback> {
                var instance = this; // (instance is the object instance on which this event object request was made)
                if (typeof instance !== 'object' || !(instance instanceof Object))
                    throw Exception.error("{Object}." + eventName, "Must be called on an Object instance.", instance);
                var graphEvent: EventDispatcher.$Type<TOwner, TCallback> = instance[privateEventName];
                if (typeof graphEvent !== 'object')
                    instance[privateEventName] = graphEvent = EventDispatcher<TOwner, TCallback>(instance, eventName, removeOnTrigger, eventTriggerCallback, UNDEFINED); // TODO: Consider the "can cancel" event.
                graphEvent.__eventPropertyName = customEventPropName;
                return graphEvent;
            };

            // ... first, set the depreciating cross-browser compatible access method ...
            type.prototype["_" + customEventPropName] = onEventProxy;

            // ... next, if supported, create the event getter property ...
            if (Compatibility.objectDefineProperty)
                Object.defineProperty(type.prototype, customEventPropName, {
                    configurable: true,
                    enumerable: true,
                    writable: true,
                    get: () => { return onEventProxy.call(this); }
                });

            return <any>{ _eventMode: eventMode, _eventName: eventName, _removeOnTrigger: removeOnTrigger }; // (the return doesn't matter at this time)
        }

        var __eventObjectInstanceCounter: number = 0;

        export class $Type<TOwner extends Object, TCallback extends EventHandler> implements IDisposable {
            /** The parent event to this event.  All event objects in the parent hierarchy participate in the capturing and bubbling event phases.
            * If this property is null, a property of the same name will be checked on the owner instead. */
            __parent: __IEventDispatcherParent;
            ///** The child events to this event.  Parent and child event objects participate in the capturing and bubbling event phases. */
            //??children: Array<EventDispatcher<TOwner, TCallback>>;

            /** The object which owns this event object. */
            owner: TOwner;
            ownerType: string;

            /** A hook to allow event owners to call the underlying handler with special arguments. 
              * If no handler is specified, then the handler is invoked with the owner as the object context, and this event object as the first parameter.
              */
            eventTriggerCallback: EventTriggerCallback<TOwner, TCallback>;

            private __eventName: string;
            __eventPropertyName: string;
            private __handlers: NativeTypes.IArray<__IEventHandlerInfo<TOwner, TCallback>> = <any>this;
            private __eventTriggerInstancePrefix: string;
            private __eventTriggerName: string;
            private __dispatchArgs: any[] = null; // (if not null, these are the additional caller args passed in the the dispatch request [used for older browsers, and the server])
            private __lastTriggerState: any = null;
            private __ownerTypePrefix: string;
            private __handlerCallCount: number;
            private __handlerCountBeforeDispatch: number;
            private __dispatchInProgress: boolean; // (holds a reference to the current handler that is being called)
            private __handlerCallInProgress: IDelegate<TOwner, TCallback> = null; // (holds a reference to the current handler that is being called)
            private __domEvent: Event; // (set to the DOM event object when a handler gets called [client side only])
            private __stop: boolean; // (true if the handler wants to cancel further handler calls)

            /** A flag set internally to determine if a UI object was already hooked into for this event (so as not to hook it twice). */
            __uiObjectHooked: boolean;

            /** Contains a list of errors if any of the callbacks fail.  */
            errors: Exception[] = null;

            /** Return the underlying event name for this event object. */
            getEventName() { return this.__eventName; }

            /** If this is true, then any new handler added will automatically be triggered as well.
            * This is handy in cases where an application state is persisted, and future handlers should always execute. */
            autoTrigger: boolean = false;

            /** Returns true if handlers exist on this event object instance. */
            hasHandlers(): boolean { return !!this.__handlers.length; }

            /** Creates an event object for a specific even type.
            * @param {TOwner} owner The owner which owns this event object.
            * @param {string} eventName The name of the event which this event object represents.
            * @param {string} eventPropertyName The name of the property that will be associated with this event, and expected on parent 
            *                 objects for the capturing and bubbling phases.  If left undefined/null, then the default is assumed to be 
            *                 'on[EventName]', where the first event character is made uppercase automatically.
            * @param {boolean} removeOnTrigger If true, then handlers are called only once, then removed (default is false).
            * @param {Function} eventTriggerCallback This is a hook which is called every time a handler needs to be called.  This exists mainly to support handlers called with special parameters.
            */
            constructor(owner: TOwner, eventName: string, public removeOnTrigger: boolean = false, eventTriggerCallback: EventTriggerCallback<TOwner, TCallback> = null, public canCancel: boolean = true) {
                if (typeof eventName !== 'string') eventName = '' + eventName;
                if (!eventName)
                    throw Exception.error("EventDispatcher", "An event name is required.", owner);
                this.__eventName = eventName;
                this.__eventPropertyName = EventDispatcher.getPropertyNameFromEventName(eventName); // (fix to support the convention of {item}.on{Event}().

                this.owner = owner;
                this.ownerType = typeof this.owner == 'object' && this.owner && typeof (<any>this.owner).getType == "function" && (<any>this.owner).getType() || "";
                this.__ownerTypePrefix = this.ownerType ? this.ownerType + "." : "";
                this.eventTriggerCallback = eventTriggerCallback;

                this.__eventTriggerInstancePrefix = '__$DS_EventTrigger_' + (__eventObjectInstanceCounter++) + "_";
                this.__eventTriggerName = this.__eventTriggerInstancePrefix + this.__eventName;
            }

            dispose(): void {
                // ... remove all handlers from the host event objects ...
                this.removeAllListeners();
                if (!document.addEventListener && document.documentElement && document.documentElement[this.__eventTriggerName])
                    delete document.documentElement[this.__eventTriggerName];
            }

            private __doEventTriggerCallback(handler: __IEventHandlerInfo<TOwner, TCallback>, args: any[]= null, doRemoveCheck: boolean = true) {
                if (this.__stop) return;
                if (!args) args = [];
                if (args && (args.length == 0 || args[args.length - 1] != this))
                    args.push(this); // (the last argument must always be this event instance)
                try {
                    var completed = false;
                    this.__handlerCallInProgress = handler;

                    if (this.eventTriggerCallback)
                        this.eventTriggerCallback(this, handler);
                    else
                        if (args)
                            handler.apply(args);
                        else
                            handler.invoke(this); // (the last argument must always be this event instance; note: this is also the fastest way to call the handler)

                    this.__handlerCallInProgress = null;
                    this.__handlerCallCount++;
                    completed = true;
                }
                finally {
                    if (!completed) {
                        var title = this.__ownerTypePrefix + "EventDispatcher<'" + this.__eventName + "'>";
                        var msg = "Error executing handler for event '" + this.__eventName + "'." + "\r\nHandler: " + handler;
                        //??Diagnostics.log(title, msg + "\r\nHandler: " + handler, Diagnostics.LogTypes.Error);
                        if (!this.errors)
                            this.errors = [];
                        this.errors.push(new Exception(title + ": " + msg, this));
                    }
                }
                if (doRemoveCheck && this.removeOnTrigger)
                    this.removeListener(handler);
            }

            /** Adds a callback to this event using the event owner as the object context. 
            * Note: The owner must be extended from "Object" (or implement the required properties) for this to work.
            */
            addListener(func: TCallback, eventMode?: EventModes);
            /** Adds an object context and callback to this event. */
            addListener(object: Object, func: TCallback, eventMode?: EventModes);
            /** Adds a delegate object to this event. */
            addListener(handler: IDelegate<TOwner, TCallback>, eventMode?: EventModes);
            addListener(_handler: any, func?: any, eventMode: EventModes = EventModes.Capture) {
                var handler: IDelegate<TOwner, TCallback>;
                // ... check if a delegate is given, otherwise attempt to create one ...
                if (typeof func == 'function')
                    handler = Delegate(_handler, func);
                else if (typeof _handler == 'function') {
                    handler = Delegate(this.owner, _handler);
                    eventMode = +func;
                } else {
                    handler = _handler;
                    eventMode = +func;
                }

                if (this.__handlers[handler.__key]) return; // (already exists if found)

                // ... if auto trigger is true, and the handler will be removed once triggered, then don't bother adding it! ...
                if (this.autoTrigger && this.removeOnTrigger) {
                    this.__doEventTriggerCallback(<__IEventHandlerInfo<TOwner, TCallback>>handler, null, false);
                    return;
                }

                var addFunctionName: string;
                var handlerInfo = <__IEventHandlerInfo<TOwner, TCallback>>handler;
                handlerInfo.__internalCallback = null;
                handlerInfo.addFunctionName = null;
                handlerInfo.eventMode = eventMode;

                // ... setup the event system depending on host support ...

                if (document.addEventListener) {
                    handlerInfo.__internalCallback = (event: Event) => {
                        if (event === void 0) event = window.event;
                        if (!(+event['__phase'] & handlerInfo.eventMode)) return;
                        var args = event['__dispatchArgs'] || this.__dispatchArgs;
                        this.__domEvent = event;
                        this.__doEventTriggerCallback(handlerInfo, args);
                        this.__domEvent = void 0;
                    };
                    document.addEventListener(this.__eventTriggerName, handlerInfo.__internalCallback);
                    handlerInfo.addFunctionName = "addEventListener";
                }
                else if (document.documentElement && (<any>document.documentElement).attachEvent) {
                    if (document.documentElement[this.__eventTriggerName] === void 0) {
                        // ... first time setup for older MSIE ...
                        document.documentElement[this.__eventTriggerName] = 0;
                    }
                    handlerInfo.__internalCallback = (event) => {
                        if (event.propertyName == this.__eventTriggerName) {
                            if (!(+event['__phase'] & handlerInfo.eventMode)) return;
                            var args = event['__dispatchArgs'] || this.__dispatchArgs;
                            this.__domEvent = event;
                            this.__doEventTriggerCallback(handlerInfo, args);
                            this.__domEvent = void 0;
                        }
                    };
                    (<any>document.documentElement).attachEvent("onpropertychange", handlerInfo.__internalCallback);
                    handlerInfo.addFunctionName = "attachEvent";
                }
                else {
                    // ... this may be server side, so just add the handler as usual ...
                    handlerInfo.addFunctionName = "{server}";
                }

                // ... register handler ...

                this[handler.__key] = this.__handlers.length; // (associate the index of this handler with the delegate key)
                this.__handlers.push(handlerInfo);

                if (this.autoTrigger)
                    this.__doEventTriggerCallback(<__IEventHandlerInfo<TOwner, TCallback>>handler);
            }

            /** Dispatch the underlying event.
            * @param {any} triggerState If supplied, the event will not trigger unless the current state is different from the last state.  This is useful in making
            * sure events only trigger once per state.  Pass in null (the default) to always dispatch regardless.  Pass 'undefined' to used the event
            * name as the trigger state (this can be used for a "trigger only once" scenario).
            * @param {boolean} canBubble Set to true to allow the event to bubble (where supported).
            * @param {boolean} canCancel Set to true if handlers can abort the event (false means it has or will occur regardless).
            * @param {string[]} args Custom arguments that will be passed on to the event handlers.
            */
            dispatchEvent(triggerState: any = null, ...args: any[]) { // TODO: Don't' use '...args'
                if (!this.setTriggerState(triggerState)) {
                    Diagnostics.log(this.__ownerTypePrefix + "EventDispatcher<'" + this.__eventName + "'>", "Request to dispatch event, but state has not changed, so ignoring request.");
                    return; // (no change in state, so ignore this request)
                }

                var parent = this.__parent || (<EventDispatcher.$Type<any, any>><any>this.owner).__parent;
                var eventChain = Array(this); // ('this' is the last for capture, and first for bubbling)
                if (parent) { // (run capture/bubbling phases)
                    var eventPropFuncName = '_' + this.__eventPropertyName;
                    while (parent) {
                        var epf = <() => EventDispatcher.$Type<any, any>>parent[eventPropFuncName];
                        if (typeof epf == 'function' && epf instanceof EventDispatcher.$Type)
                            eventChain.push(epf());
                        parent = <any>parent.__parent;
                    }
                }

                // ... do capture phase ...
                for (var n = eventChain.length, i = n - 1; i >= 0; --i) {
                    var dispatcher = eventChain[i];
                    if (dispatcher.__handlers.length)
                        dispatcher.__dispatchEvent.call(dispatcher, args, EventModes.Capture);
                }

                // ... do bubbling phase ...
                for (var i = 0, n = eventChain.length; i < n; ++i) {
                    var dispatcher = eventChain[i];
                    if (dispatcher.__handlers.length)
                        dispatcher.__dispatchEvent.call(dispatcher, args, EventModes.Bubble);
                }
            }

            private __dispatchEvent(args: any[], phase: EventModes) {
                var log = Diagnostics.log(this.__ownerTypePrefix + "EventDispatcher<'" + this.__eventName + "'>", "Dispatching event '" + this.__eventName + "' (handlers: " + (this.__handlers ? this.__handlers.length : 0) + ") ... ").beginCapture();

                if (!this.__handlers || !this.__handlers.length) {
                    log.write("There are no registered handlers for this event.");
                    return;
                }

                this.__dispatchArgs = args;
                this.__handlerCountBeforeDispatch = this.__handlers.length; // ('__handlers.length' can change if 'removeOnTrigger' is set to true)
                this.__handlerCallCount = 0;

                this.__dispatchInProgress = true;
                this.__stop = false;

                if (document.addEventListener) {
                    var event = document.createEvent("UIEvents");
                    event['__sender'] = this;
                    event['__dispatchArgs'] = args;
                    event['__phase'] = phase;
                    event.initEvent(this.__eventTriggerName, false, this.canCancel);
                    document.dispatchEvent(event);
                }
                else if (document.documentElement && (<any>document.documentElement).attachEvent) {
                    window.event['__sender'] = this;
                    window.event['__dispatchArgs'] = args;
                    window.event['__phase'] = phase;
                    document.documentElement[this.__eventTriggerName]++; // (triggers the 'onpropertychange' event for MSIE)
                }
                else { // server side ...
                    if (this.errors)
                        this.errors = null; // (clear any past errors before continuing)

                    for (var i = 0, n = this.__handlers.length; i < n; ++i)
                        if (this.__handlers[i].eventMode == phase)
                            this.__doEventTriggerCallback(this.__handlers[i], args);
                }

                if (this.__handlerCallCount !== this.__handlerCountBeforeDispatch)
                    throw Exception.from(this.__ownerTypePrefix + "EventDispatcher<'" + this.__eventName + "'>: Assertion error: The number of handlers called does not match the number of registered handlers.", this);

                this.__dispatchInProgress = false;

                log.write("Done dispatching event '" + this.__eventName + "'.", Diagnostics.LogTypes.Success).parent.endCapture();
            }

            /** If the given state value is different from the last state value, the internal trigger state value will be updated, and true will be returned.
            * If a state value of null is given, the request will be ignored, and true will always be returned.
            * If you don't specify a value ('undefined' is received) then the internal event name becomes the trigger state value (this can be used for a "trigger
            * only once" scenario).  Use 'resetTriggerState()' to reset the internal trigger state when needed.
            */
            setTriggerState(triggerState?: any): boolean {
                if (triggerState === void 0) triggerState = this.__eventTriggerName;
                if (triggerState !== null)
                    if (triggerState === this.__lastTriggerState)
                        return false; // (no change in state, so ignore this request)
                    else
                        this.__lastTriggerState = triggerState;
                return true;
            }

            /** Resets the current internal trigger state to null. The next call to 'setTriggerState()' will always return true.
            * This is usually called after a sequence of events have completed, in which it is possible for the cycle to repeat.
            */
            resetTriggerState() { this.__lastTriggerState = null; }

            /** A simple way to pass arguments to event handlers using arguments with static typing (calls 'dispatchEvent(null, false, false, arguments)').
            * TIP: To prevent triggering the same event multiple times, use a custom state value in a call to 'setTriggerState()', and only call
            * 'dispatch()' if true is returned (example: "someEvent.setTriggerState(someState) && someEvent.dispatch(...);", where the call to 'dispatch()'
            * only occurs if true is returned from the previous statement).
            */
            dispatch: TCallback = <TCallback><any>((...args: any[]) => {
                this.dispatchEvent.apply(this, args.unshift(null));
            });

            /** If called within a handler, prevents the other handlers from being called. */
            stop(): void {
                if (this.__dispatchInProgress)
                    this.__stop = true;
            }

            private __indexOf(object: {}, handler: TCallback) {
                for (var i = this.__handlers.length - 1; i >= 0; --i) {
                    var d = this.__handlers[i];
                    if (d.object == object && d.func == <any>handler)
                        return i;
                }
                return -1;
            }

            private __removeListener(i: number) {
                if (i >= 0 && i < this.__handlers.length) {
                    var handlerInfo = (i == this.__handlers.length - 1 ? this.__handlers.pop() : this.__handlers.splice(i, 1)[0]);

                    if (this.__handlerCallInProgress && this.__handlerCallInProgress != handlerInfo)
                        --this.__handlerCountBeforeDispatch; // (if the handler being removed is not the current one in progress, then it will never be called, and thus the original count needs to change)

                    if (handlerInfo.addFunctionName == "addEventListener")
                        document.removeEventListener(this.__eventName, handlerInfo.__internalCallback, false);
                    else if (handlerInfo.addFunctionName == "attachEvent")
                        (<any>document.documentElement).detachEvent("onpropertychange", handlerInfo.__internalCallback);
                    // (else this is most likely the server side, and removing it from the array is good enough)
                    this[handlerInfo.__key] = void 0; // (faster than deleting it, and prevents having to create the property over and over)
                }
            }

            removeListener(object, Object, func: TCallback);
            removeListener(handler: IDelegate<TOwner, TCallback>);
            removeListener(handler: IDelegate<TOwner, TCallback>, func?: TCallback) {
                // ... check if a delegate is given, otherwise attempt to create one ...
                if (typeof func == 'function') {
                    this.__removeListener(this.__indexOf(handler, func));
                } else {
                    this.__removeListener(this.__indexOf(handler.object, <TCallback><Function>handler.func));
                }
            }

            removeAllListeners() {
                for (var i = this.__handlers.length - 1; i >= 0; --i)
                    this.__removeListener(i);
            }
        }

        AppDomain.registerClass(EventDispatcher, [DreamSpace, System]);
    }

    // =======================================================================================================================
}

// ###########################################################################################################################
